package com.atguigu.gull.gmall.gateway.filter;

import com.alibaba.nacos.client.utils.IpUtil;
import com.atguigu.gmall.common.exception.GmallException;
import com.atguigu.gmall.common.utils.IpUtils;
import com.atguigu.gmall.common.utils.JwtUtils;
import com.atguigu.gull.gmall.gateway.properties.JwtProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * 登录状态验证的filter：
 * 1.访问目标服务是，如果请求的路由无需登录状态验证，放行请求
 * 2.访问目标服务的请求需要验证登录状态：
 * 2.1 已登录
 * 2.1.1 已登录 客户端的ip和token中的ip相同 放行请求
 * 2.1.2 已登录 客户端ip和token的ip不同 拦截请求
 * 2.2 未登录 拦截请求
 * <p>
 * 上面获取token解析时，如果有异常(token过期了或者被篡改了)拦截请求
 * 拦截请求；重定向跳转登录页面重新登录即可，登录成功后还需要跳转未登录之前的燕麦奶(重定向配置returnUrl的参数值，值为登录之前的url地址)
 * <p>
 * 暂时使用auth服务的路由测试：
 * 给auth服务路由设置无需登录验证的允许访问的路径地址(toLogin.html,login)
 * 实际使用，这个过滤器是要加在别的服务上的，按个服务需要登录，就放在那个服务器上
 */
@Configuration
@Slf4j
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {

    @Autowired
    private JwtProperties jwtProperties;


    public AuthGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override//过滤方法
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                //1.判断请求的路径 是否是配置中的允许直接访问的无需验证登录的 地址，如果是则放行请求
                //2.验证登录状态
                //2.1验证token是否有效(是否过期、是否被篡改）
                //再验证token中的ip地址和当前请求的客户端ip是否一致，如果一致代表已登录 放行
                //2.2token无效或者ip不匹配 登录状态无效 拦截请求 重定向到登录页面

                ServerHttpRequest request = exchange.getRequest();//拿到request对象
                ServerHttpResponse response = exchange.getResponse();//拿到response对象

                try {
                    //1.判断请求的路径是否需要登录，不需要直接放行
                /*请求报文中的访问路径(服务器地址后的、?参数前的内容)，如http://sso.gmall.com/toLogin?return_url=http://www.gmall.com,
                  toLogin.html 就是path ,sso.gmall.com就是host(服务器地址) */
                    String path = request.getURI().getPath();
                    //application.yml中配置的路径地址集合
                    List<String> paths = config.getPaths();
                    if (paths.stream().anyMatch(configPath -> configPath.indexOf(path) != -1)) {//即paths.contain(path)
                        return chain.filter(exchange);//放行请求
                    }

                    //2.验证登录状态，即验证cookie中的token
                    //返回值是集合，因为，cookie里面的可以同名，一个key对应多个value，我们没有同名文件，直接获取第一个即可
                    /*List<HttpCookie> cookies = request.getCookies().get(jwtProperties.getCookieTokenName());
                    HttpCookie httpCookie = cookies.get(0);*/
                    //上述太臃肿，直接一行代码代替
                    HttpCookie httpCookie = request.getCookies().getFirst(jwtProperties.getCookieTokenName());
                    String token = httpCookie.getValue();
                    if (httpCookie == null || StringUtils.isEmpty(token)) {//非空校验
                        throw new GmallException("没有携带Gamll-Token访问");
                    }
                    Map<String, Object> map = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
                    System.out.println("authFilter" + map);
                    //校验ip
                    String tokenIp = map.get("ip").toString();//token中的ip
                    //InetSocketAddress clientIp = request.getLocalAddress();//从请求报文中获取当前ip
                    String clientIp = IpUtils.getIpAddressAtGateway(request);//我们使用自己的工具类，不用上行代码
                    if (!clientIp.equals(tokenIp)) {
                        throw new GmallException("ip地址校验失败,clientIp=" + clientIp + ",tokenIp=" + tokenIp);
                    }
                    //上述问题都未发生，直接放行
                    return chain.filter(exchange);
                } catch (Exception e) {
                    e.printStackTrace();
                    log.warn(e.getMessage());
                    /*token登录状态无效，过滤器拦截它，不放行，并从定向到登录页面
                     即让浏览器访问http://sso.gmall.com/toLogin?redirect_url=本地请求地址
                     如果是 HttpServletResponse response对象，
                     直接response.sendRedirect("http://sso.atguigu.com/toLogin?redirect_url=" + request.getRequestURL().toString())
                     这里我们用的是 ServerHttpResponse response对象，所以需要
                     向响应报文中添加一对头(响应状态码302，重定向地址location)，以此来设置重定向*/
                    response.getHeaders().set(HttpHeaders.LOCATION,
                            "http://sso.gmall.com/toLogin?redirect_url=" +request.getURI());//设置重定向地址
                    response.setStatusCode(HttpStatus.SEE_OTHER);//设置响应报文的状态码为302
                    Mono<Void> voidMono = response.setComplete();
                    return voidMono;
                }

            }
        };
    }

    @Data
    public static class Config {
        private List<String> paths;
    }

    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("paths");
    }
}
